/**
 * @license
 * Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file Listener model to spatialize sources in an environment.
 * @author Andrew Allen <bitllama@google.com>
 */

'use strict';


// Internal dependencies.
const Omnitone = require('../node_modules/omnitone/build/omnitone.js');
const Encoder = require('./encoder.js');
const Utils = require('./utils.js');


/**
 * @class Listener
 * @description Listener model to spatialize sources in an environment.
 * @param {AudioContext} context
 * Associated {@link
https://developer.mozilla.org/en-US/docs/Web/API/AudioContext AudioContext}.
 * @param {Object} options
 * @param {Number} options.ambisonicOrder
 * Desired ambisonic order. Defaults to
 * {@linkcode Utils.DEFAULT_AMBISONIC_ORDER DEFAULT_AMBISONIC_ORDER}.
 * @param {Float32Array} options.position
 * Initial position (in meters), where origin is the center of
 * the room. Defaults to
 * {@linkcode Utils.DEFAULT_POSITION DEFAULT_POSITION}.
 * @param {Float32Array} options.forward
 * The listener's initial forward vector. Defaults to
 * {@linkcode Utils.DEFAULT_FORWARD DEFAULT_FORWARD}.
 * @param {Float32Array} options.up
 * The listener's initial up vector. Defaults to
 * {@linkcode Utils.DEFAULT_UP DEFAULT_UP}.
 */
function Listener(context, options) {
  // Public variables.
  /**
   * Position (in meters).
   * @member {Float32Array} position
   * @memberof Listener
   * @instance
   */
  /**
   * Ambisonic (multichannel) input {@link
   * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
   * @member {AudioNode} input
   * @memberof Listener
   * @instance
   */
  /**
   * Binaurally-rendered stereo (2-channel) output {@link
   * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
   * @member {AudioNode} output
   * @memberof Listener
   * @instance
   */
  /**
   * Ambisonic (multichannel) output {@link
   * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}.
   * @member {AudioNode} ambisonicOutput
   * @memberof Listener
   * @instance
   */
  // Use defaults for undefined arguments.
  if (options == undefined) {
    options = {};
  }
  if (options.ambisonicOrder == undefined) {
    options.ambisonicOrder = Utils.DEFAULT_AMBISONIC_ORDER;
  }
  if (options.position == undefined) {
    options.position = Utils.DEFAULT_POSITION.slice();
  }
  if (options.forward == undefined) {
    options.forward = Utils.DEFAULT_FORWARD.slice();
  }
  if (options.up == undefined) {
    options.up = Utils.DEFAULT_UP.slice();
  }

  // Member variables.
  this.position = new Float32Array(3);
  this._tempMatrix3 = new Float32Array(9);

  // Select the appropriate HRIR filters using 2-channel chunks since
  // multichannel audio is not yet supported by a majority of browsers.
  this._ambisonicOrder =
    Encoder.validateAmbisonicOrder(options.ambisonicOrder);

    // Create audio nodes.
  this._context = context;
  if (this._ambisonicOrder == 1) {
    this._renderer = Omnitone.Omnitone.createFOARenderer(context, {});
  } else if (this._ambisonicOrder > 1) {
    this._renderer = Omnitone.Omnitone.createHOARenderer(context, {
      ambisonicOrder: this._ambisonicOrder,
    });
  }

  // These nodes are created in order to safely asynchronously load Omnitone
  // while the rest of the scene is being created.
  this.input = context.createGain();
  this.output = context.createGain();
  this.ambisonicOutput = context.createGain();

  // Initialize Omnitone (async) and connect to audio graph when complete.
  let that = this;
  this._renderer.initialize().then(function() {
    // Connect pre-rotated soundfield to renderer.
    that.input.connect(that._renderer.input);

    // Connect rotated soundfield to ambisonic output.
    if (that._ambisonicOrder > 1) {
      that._renderer._hoaRotator.output.connect(that.ambisonicOutput);
    } else {
      that._renderer._foaRotator.output.connect(that.ambisonicOutput);
    }

    // Connect binaurally-rendered soundfield to binaural output.
    that._renderer.output.connect(that.output);
  });

  // Set orientation and update rotation matrix accordingly.
  this.setOrientation(options.forward[0], options.forward[1],
    options.forward[2], options.up[0], options.up[1], options.up[2]);
};


/**
 * Set the source's orientation using forward and up vectors.
 * @param {Number} forwardX
 * @param {Number} forwardY
 * @param {Number} forwardZ
 * @param {Number} upX
 * @param {Number} upY
 * @param {Number} upZ
 */
Listener.prototype.setOrientation = function(forwardX, forwardY, forwardZ,
  upX, upY, upZ) {
  let right = Utils.crossProduct([forwardX, forwardY, forwardZ],
    [upX, upY, upZ]);
  this._tempMatrix3[0] = right[0];
  this._tempMatrix3[1] = right[1];
  this._tempMatrix3[2] = right[2];
  this._tempMatrix3[3] = upX;
  this._tempMatrix3[4] = upY;
  this._tempMatrix3[5] = upZ;
  this._tempMatrix3[6] = forwardX;
  this._tempMatrix3[7] = forwardY;
  this._tempMatrix3[8] = forwardZ;
  this._renderer.setRotationMatrix3(this._tempMatrix3);
};


/**
 * Set the listener's position and orientation using a Three.js Matrix4 object.
 * @param {Object} matrix4
 * The Three.js Matrix4 object representing the listener's world transform.
 */
Listener.prototype.setFromMatrix = function(matrix4) {
  // Update ambisonic rotation matrix internally.
  this._renderer.setRotationMatrix4(matrix4.elements);

  // Extract position from matrix.
  this.position[0] = matrix4.elements[12];
  this.position[1] = matrix4.elements[13];
  this.position[2] = matrix4.elements[14];
};


module.exports = Listener;
